/*********************************************************
          File Name:hash.c
          Author: Abby Cin
          Mail: abbytsing@gmail.com
          Created Time: Sat 01 Aug 2015 09:17:03 PM CST
**********************************************************/

#include "hash.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

hash_node_t *hash_node_new(void)
{
        hash_node_t *res = NULL;
        res = (hash_node_t *)malloc(sizeof(hash_node_t));
        memset(res, 0, sizeof(hash_node_t));
        res->next = NULL;

        return res;
}

void hash_node_delete(hash_node_t *node)
{
        free(node->key);
        free(node->val);
        free(node);

        node = NULL;
}

hash_table_t *hash_table_new(void)
{
        hash_table_t *table = NULL;
        table = (hash_table_t *)malloc(sizeof(hash_table_t));
        if(table == NULL)
                return NULL;

        table->elements = DEFAULT_NUM;
        table->key_ratio = DEFAULT_RATIO;
        table->in_use = 0;
        table->node = (hash_node_t **)calloc(DEFAULT_NUM,
                        sizeof(hash_node_t *));
        int i;
        for(i = 0; i < DEFAULT_NUM; i++)        // all NULL
                table->node[i] = NULL;
        if(table->node == NULL)
        {
                free(table);
                table = NULL;
                return NULL;
        }

        return table;
}

void hash_table_delete(hash_table_t *table)
{
        unsigned int i;
        unsigned int n = table->elements;
        hash_node_t *tmp, *head, *cur;

        for(i = 0; i < n; i++)
        {
                if(table->node[i] != NULL)   // a list here
                {
                        head = table->node[i];   // get list head
                        cur = head->next;
                        while(cur != NULL)
                        {
                                tmp = cur;
                                cur = cur->next;
                                hash_node_delete(tmp);
                        }
                        hash_node_delete(table->node[i]);
                }
        }

        free(table->node);
        table->node = NULL;
        free(table);
        table = NULL;
}


int hash_resize(hash_table_t *table, unsigned int len)
{
        hash_node_t **node, **tmp, *cur, *del;
        unsigned int count;
        unsigned int n = len / 2, i;

        count = hash_getpairs(table, &node);
        if(count == 0)
                return -1;
        tmp = table->node;
        table->node = (hash_node_t **)calloc(len, sizeof(hash_node_t *));
        if(table->node == NULL)
        {
                table->node = tmp;
                free(node);
                return -2;
        }
        table->elements = len;
        table->in_use = 0;
        while(count--)
                hash_add(table, node[count]->key, node[count]->val);

        for(i = 0; i < n; i++)
        {
                if(tmp[i] != NULL)   // a list here
                {
                        cur = tmp[i]->next;
                        while(cur != NULL)
                        {
                                del = cur;
                                cur = cur->next;
                                hash_node_delete(del);
                        }
                        hash_node_delete(tmp[i]);
                }
                
        }
        free(tmp);
        tmp = NULL;
        free(node);
        node = NULL;

        return 0;
}

int hash_add(hash_table_t *table, char *key, char *value)
{
        unsigned int key_len = strlen(key) + 1;
        unsigned int value_len = strlen(value) + 1;

        if(key_len >= 31)
                return -1;      // key too long

        if(table->in_use / table->elements >= table->key_ratio)
                hash_resize(table, table->elements * 2);

        unsigned long hash = do_hash(key) % table->elements;
        hash_node_t *node = table->node[hash];

        while(node != NULL)
        {
                // already exists
                if(memcmp(node->key, key, key_len) == 0 &&
                                memcmp(node->val, value, value_len) == 0)
                        return -1;
                // replace value
                if(memcmp(node->key, key, key_len) == 0 && 
                                memcmp(node->val, value, value_len) != 0)
                {
                        free(node->val);
                        node->val = (char *)malloc(value_len);
                        memcpy(node->val, value, value_len);
                        return -1;
                }
                node = node->next;
        }

        node = hash_node_new();
        if(node == NULL)
                return -1;
        node->key = (char *)malloc(key_len);
        node->val = (char *)malloc(value_len);
        if(node->key == NULL || node->val == NULL)
                return -1;

        memcpy(node->key, key, key_len);
        memcpy(node->val, value, value_len);
        node->key[key_len - 1] = '\0';
        node->val[value_len -1] = '\0';
        node->next = NULL;
        
        node->next = table->node[hash];
        table->node[hash] = node;
        table->in_use += 1;

        return 0;
}

int hash_remove(hash_table_t *table, char *key)
{
        unsigned int key_len = strlen(key);
        unsigned long hash = do_hash(key) % table->elements;

        hash_node_t *tmp = table->node[hash];
        hash_node_t *hold = NULL;

        // key in list
        for(;tmp != NULL;)
        {
                if(memcmp(tmp->key, key, key_len) == 0)
                {
                        if(hold == NULL)        // head
                                table->node[hash] = tmp->next;
                        else
                                hold->next = tmp->next;
                        hash_node_delete(tmp);
                        return 0;
                }
                hold = tmp;
                tmp = tmp->next;
        }

        return -1;      // not found
}

char *hash_getval(hash_table_t *table, char *key)
{
        unsigned int key_len = strlen(key);
        unsigned long hash = do_hash(key) % table->elements;
        hash_node_t *tmp = table->node[hash];

        // test if exist conflict
        for(;tmp != NULL;)
        {
                if(memcmp(tmp->key, key, key_len) == 0)
                        return tmp->val;
                tmp = tmp->next;
        }
        // this `hash` position has a list, and not found `key` in the list
        return NULL;
}

int hash_getpairs(hash_table_t *table, hash_node_t ***node)
{
        unsigned int i;
        int count = 0;
        hash_node_t *tmp = NULL;

        *node = (hash_node_t **)calloc(table->in_use,
                        sizeof(hash_node_t *));
        if(*node == NULL)
                return -1;

        for(i = 0, count = 0; i < table->elements; i++)
        {
                if(table->node[i] != NULL)      // found key at `hash` postion
                {
                        (*node)[count++] = table->node[i];
                        tmp = table->node[i];
                        // travle in list at `hash` postion
                        while(tmp->next != NULL)
                        {
                                (*node)[count++] = tmp->next;
                                tmp = tmp->next;
                        }
                }
        }

        return count;   // how many keys
}

static char *get_file_buf(const char *path)
{
        FILE *fp = NULL;
        char *buf = NULL;
        long size;

        fp = fopen(path, "r");
        if(fp == NULL)
                return NULL;
        fseek(fp, 0L, SEEK_END);
        size = ftell(fp);
        rewind(fp);

        buf = (char *)malloc(size);
        if(buf == NULL)
        {
                fclose(fp);
                return NULL;
        }

        fread(buf, size, 1, fp);
        fclose(fp);

        buf[size -1] = '\0';
        return buf;
}

static line_t get_line_buf(char *buf, char **line, size_t *offset)
{
        char *tmp = buf + *offset;
        long i = 0;

        switch(*tmp)
        {
                case '\0':
                    return LINE_EOL;
                case '\n':
                case '\r':
                    *offset += 1;
                    return LINE_CMT;
                case '#':
                    while(*tmp != '\n' && *tmp != '\0')
                    {
                        *offset += 1;
                        tmp += 1;
                    }
                    return LINE_CMT;
        }

        for(; tmp[i] != '\n'; ++i)
                *offset += 1;
        i++;                            // including '\n'
        *line = (char *)malloc(i);
        if(*line == NULL)
                return LINE_BAD;
        memset(*line, 0, i);

        memcpy(*line, tmp, i);
        (*line)[i-1] = '\0';
        
        return LINE_OK;
}

static int parse_line(char *line, hash_table_t *ht)
{
        int i = 0;
        char *hold = line;
        char *key = NULL, *value = NULL;

        while(line[i] != '=')
                i++;
        if((key = (char *)malloc(i)) == NULL)
                return -1;
        memset(key, 0, i);
        memcpy(key, line, i);
        key[i-1] = '\0';

        line += i + 2;          // seek to key starter

        i = 0;
        while(line[i] != '\n' && line[i] != '#' && line[i] != '\0')
                i++;
        i += 1;
        if((value = (char *)malloc(i)) == NULL)
        {
                free(key);
                return -1;
        }
        memset(value, 0, i);
        memcpy(value, line, i);
        value[i-1] = '\0';

        if(hash_add(ht, key, value) == -1)
        {
                free(key);
                free(value);
                free(hold);
                return -1;
        }

        free(hold);
        free(key);
        free(value);

        return 0;
}

int file2hash(const char *path, hash_table_t *ht, size_t *offset)
{
        char *filebuf = NULL;
        char *line = NULL;
        line_t res;
        int breakall = 0;

        filebuf = get_file_buf(path);
        if(filebuf == NULL)
                return -1;

        for(;breakall == 0;)
        {
                res = get_line_buf(filebuf, &line, offset);
                switch(res)
                {
                        case LINE_EOL:
                                breakall = 1;
                                break;
                        case LINE_BAD:
                                return -1;
                        case LINE_CMT:
                                break;
                        case LINE_OK:
                                if(parse_line(line, ht) == -1)
                                        return -1;
                                break;
                }
        }
        free(filebuf);
        return 0;
}
